iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Vibe Coding

做一個團購後端,順便搞懂那些事系列 第 5

Day 5|為什麼 Controller 不能直接回傳 Entity?

  • 分享至 

  • xImage
  •  

Entity不會直接拿來接收請求(Request)或是回傳請求(Response),因為如果直接回傳Entity會暴露資料表的欄位,同理,直接讓Entity接收請求的參數值,沒有經過任何檢查,就把資料存進資料庫,會有mass assignment的風險。因此不管是Request還是Response都要在Entity前面新增一個DTO()為資料把關。

Response端的資料處理

商品分類由管理員維護,新增一筆之後前端要拿到剛建立的資料。Controller 呼叫完 Service 手上已有 Category,最短的寫法是直接回出去。

@PostMapping("/category/create_category")
public ResponseEntity<Category> createCategory(@Valid @RequestBody CategoryRequest request,@AuthenticationPrincipal String operator) {
    Category created = categoryService.createCategory(request, operator);
    return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

這段程式可以正常運作,Jackson 會把每個欄位序列化成 JSON。

Category 照著 categories 表長,九個欄位會全部進到回應裡。查詢分類清單時,任何登入帳號收到的是:

{
  "categoryId": 3,
  "name": "飲料",
  "sortOrder": 1,
  "version": 0,
  "isDeleted": 0,
  "createdBy": "admin",
  "createdAt": "2025-05-02T10:14:33",
  "updatedBy": null,
  "updatedAt": "2025-05-02T10:14:33"
}

事實上,前端會用到的就只有前三個,這時候回傳 entity 等於告訴大家「API 回應的形狀就是資料表的形狀」。sort_order 哪天拆成兩欄,JSON 的 key 就跟著變,原本和前端約定好的API契約同時壞掉。schema 是內部實作,本該可以自由調整;API 契約是對外接口,改動要協調。綁在一起,資料庫重構就多出一輪跨團隊協調。因此,對外只留前端有理由知道的欄位,而不是把整個Entity傳出去。

做法是新增一個ResponseDTO:

public record CategoryResponse(Integer categoryId, String name, Integer sortOrder, Integer version) {
    public static CategoryResponse from(Category c) {
        return new CategoryResponse(c.getCategoryId(), c.getName(), c.getSortOrder(), c.getVersion());
    }
}

Request端的資料處理

回應要收斂,請求更要。若接收端直接收 entity:

public Category create(@RequestBody Category category) { ... }

body 裡有哪個 key,就往物件同名的欄位填。完全不區分哪些欄位該由 client端決定、完全沒檢查丟過來的值是否正確,多打幾個 key 就照單全收,資料從請求進到資料庫裏面時,完全沒有隔離,下面列出沒有隔離的後果:

{ "name": "飲料", "sortOrder": 1, "createdBy": "alice", "isDeleted": 1, "categoryId": 3 }
多塞的欄位 後果
createdBy 稽核欄位變成 client 說了算,事後追查會指向別人
isDeleted 建出一筆一出生就是已刪除狀態的資料
categoryId 主鍵本來由資料庫發號,變成 client 指定

不如換一個只宣告 client 該填欄位的型別,新增一個RequestDTO:

@Data
public class CategoryRequest {
    @NotBlank(message = "必須輸入 name")
    private String name;

    @NotNull(message = "必須輸入 sortOrder")
    private Integer sortOrder;
}

createdBy 在這裡沒有地方落腳,它由伺服器從登入身分取得,client 無從偽造。

三種物件由誰決定形狀

同一筆新增請求上有三種物件,各自活在不同深度:

https://ithelp.ithome.com.tw/upload/images/20260919/20168667erA159MXgR.png

Entity 確實會回到 Controller,Service 交回來的就是 Category。分界不在看不看得到,而是它進得來,出不去

類型 職責 誰決定它的形狀
Request DTO client 能提供什麼、驗證規則 API 契約
Entity 對應資料表結構 資料庫 schema
Response DTO client 該看到什麼 API 契約

代價是每個端點都要多寫一次轉換,欄位增減時兩邊都得改;換到的是改資料表不必通知前端。


上一篇
Day 4|分類名稱軟刪除之後,名字還能重新使用?
下一篇
Day 6|例外設計:讓錯誤一路走到統一出口
系列文
做一個團購後端,順便搞懂那些事8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言